package com.facebook.datasource;

import android.util.Pair;

import com.facebook.common.internal.Preconditions;

import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;

import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;

public abstract class AbstractDataSource<T> {
    @GuardedBy(value = "this")
    private DataSourceStatus mDataSourceStatus = DataSourceStatus.IN_PROGRESS;
    @GuardedBy(value = "this")
    private boolean mIsClosed = false;
    @Nullable
    @GuardedBy(value = "this")
    private T mResult = null;
    @GuardedBy(value = "this")
    private Throwable mFailureThrowable = null;
    @GuardedBy(value = "this")
    private float mProgress = 0f;
    private ConcurrentLinkedQueue<Pair<DataSubscriber<T>, Executor>> mSubscribers = new ConcurrentLinkedQueue();

    enum DataSourceStatus {
        IN_PROGRESS,
        SUCCESS,
        FAILURE
    }

    public AbstractDataSource() {
    }

    private void notifyDataSubscriber(final DataSubscriber<T> dataSubscriber,
                                      Executor executor,
                                      final boolean isFailure,
                                      final boolean isCancellation) {
        executor.execute(new Runnable() {
            @Override
            public final void run() {
                if (isFailure) {
                    dataSubscriber.onFailure(AbstractDataSource.this);
                } else if (!isCancellation) {
                    dataSubscriber.onNewResult(AbstractDataSource.this);
                }
            }
        });
    }

    @Nullable
    public synchronized T getResult() {
        return this.mResult;
    }

    public final void subscribe(DataSubscriber<T> dataSubscriber, Executor executor) {
        Preconditions.checkNotNull(dataSubscriber);
        Preconditions.checkNotNull(executor);
        boolean shouldNotify = false;
        synchronized (this) {
            if (this.mIsClosed) {
                return;
            }
            if (this.mDataSourceStatus == DataSourceStatus.IN_PROGRESS) {
                this.mSubscribers.add(Pair.create(dataSubscriber, executor));
            }
            shouldNotify = hasResult() || isFinished() || wasCancelled();
        }
        if (shouldNotify) {
            this.notifyDataSubscriber(dataSubscriber, executor, this.hasFailed(), this.wasCancelled());
        }
    }

    public boolean setResult(T value, boolean isLast) {
        boolean result = this.setResultInternal(value, isLast);
        if (result) {
            this.notifyDataSubscribers();
        }
        return result;
    }

    public boolean setFailure(Throwable throwable) {
        boolean result = this.setFailureInternal(throwable);
        if (result) {
            this.notifyDataSubscribers();
        }
        return result;
    }

    /**
     * finally 中不能又return 语句
     *
     * @param value
     * @param isLast
     * @return
     */
    private synchronized boolean setResultInternal(T value, boolean isLast) {
        try {
            if (this.mIsClosed || this.mDataSourceStatus != DataSourceStatus.IN_PROGRESS) {
                return false;
            } else {
                if (isLast) {
                    this.mDataSourceStatus = DataSourceStatus.SUCCESS;
                    this.mProgress = 1f;
                }
                T previousResult = null;
                if (this.mResult != value) {
                    previousResult = this.mResult;
                    this.mResult = value;
                    value = previousResult;
                } else {
                    value = previousResult;
                }
                return true;
            }
        } finally {
            if (value != null) {
                this.closeResult();
            }
        }
    }

    private synchronized boolean setFailureInternal(Throwable throwable) {
        if (this.mIsClosed) {
            return false;
        } else if (this.mDataSourceStatus == DataSourceStatus.IN_PROGRESS) {
            this.mDataSourceStatus = DataSourceStatus.FAILURE;
            this.mFailureThrowable = throwable;
            return true;
        } else {
            return false;
        }
    }

    public boolean hasResult() {
        return mResult != null;
    }

    public final boolean isFinished() {
        return mDataSourceStatus != DataSourceStatus.IN_PROGRESS;
    }

    @Nullable
    public synchronized final Throwable getFailureCause() {
        return this.mFailureThrowable;
    }

    public final float getProgress() {
        return mProgress;
    }

    public boolean close() {
        T resultToClose;
        synchronized (this) {
            if (this.mIsClosed) {
                return false;
            }
            this.mIsClosed = true;
            resultToClose = this.mResult;
            this.mResult = null;
        }
        if (resultToClose != null) {
            this.closeResult();
        }
        if (!this.isFinished()) {
            this.notifyDataSubscribers();
        }
        synchronized (this) {
            this.mSubscribers.clear();
            return true;
        }

    }

    public synchronized final boolean isClosed() {
        return mIsClosed;
    }

    public void closeResult() {
    }

    private synchronized boolean hasFailed() {
        return mDataSourceStatus == DataSourceStatus.FAILURE;
    }

    private void notifyDataSubscribers() {
        boolean isFailure = this.hasFailed();
        boolean isCancellation = this.wasCancelled();
        Iterator<Pair<DataSubscriber<T>, Executor>> iterator = this.mSubscribers.iterator();
        while (iterator.hasNext()) {
            Pair<DataSubscriber<T>, Executor> pair = iterator.next();
            this.notifyDataSubscriber(pair.first, pair.second, isFailure, isCancellation);
        }
    }

    private boolean wasCancelled() {
        synchronized (this) {
            if (!this.isClosed()) {
                return false;
            } else if (this.isFinished()) {
                return false;
            }
            return true;
        }
    }
}

